package de.bk.timecalc.core;

import java.util.Random;

/**
 * Core logic of TimeCalc.
 * 
 * Test: {@link TimeCalcCoreTest}.
 * 
 * @author Bastian Krol
 * @version $Revision:$ $Date:$ $Author:$
 */
public class TimeCalcCore
{
  private static final Random RANDOM = new Random();
  private static final int MAX_SECONDS = 20 * 60;

  public static final ProbabilisticDistribution<Item> DEFAULT_ITEM_DISTRIBUTION;
  static
  {
    DEFAULT_ITEM_DISTRIBUTION = new ProbabilisticDistribution<Item>();
    DEFAULT_ITEM_DISTRIBUTION.add(Item.RED_ARMOR, 2);
    DEFAULT_ITEM_DISTRIBUTION.add(Item.MEGA_HEALTH, 2);
    DEFAULT_ITEM_DISTRIBUTION.add(Item.QUAD, 1);
    DEFAULT_ITEM_DISTRIBUTION.normalize();
  }

  private TimerDirection timerDirection;

  private ChallengeDTO lastChallenge;
  private TimeDTO correctAnswer;
  private Duration timeNeededForAnswer;

  private int challengesDone;
  private float scoreForLastAnswer;
  private float score;

  private IScoringSchema scoringSchema;
  private StopWatch stopWatch;

  private ProbabilisticDistribution<Item> itemDistribution;
  private boolean roundToFiveSeconds;

  /**
   * Creates a new TimeCalcCore object.
   */
  public TimeCalcCore()
  {
    this.timerDirection = TimerDirection.UP;
    this.scoringSchema = new DefaultScoringSchema();
    this.stopWatch = new StopWatch();
    this.itemDistribution = DEFAULT_ITEM_DISTRIBUTION;
    this.roundToFiveSeconds = false;
  }

  /**
   * Starts the next time calc challenge.
   */
  public ChallengeDTO nextChallenge()
  {
    Item item = this.itemDistribution.select();
    TimeDTO time = this.createRandomTime(item);
    this.lastChallenge = new ChallengeDTO(time, item);
    this.correctAnswer = this.calculateAnswer(this.lastChallenge);
    this.stopWatch.start();
    return this.lastChallenge;
  }

  public void pause()
  {
    this.stopWatch.pause();
  }

  public void startAgainAfterPause()
  {
    this.stopWatch.start();
  }

  private TimeDTO createRandomTime(Item difference)
  {
    int randomSeconds;
    switch (this.timerDirection)
    {
      case DOWN:
        int minSeconds = difference.getSeconds();
        // generate a random number between minSeconds and MAX_SECONDS
        randomSeconds = RANDOM.nextInt(MAX_SECONDS - minSeconds) + minSeconds;
        break;
      case UP:
        // generate a random number between 0 and max seconds
        randomSeconds = RANDOM.nextInt(MAX_SECONDS - difference.getSeconds());
        break;
      default:
        throw new AssertionError();
    }

    if (this.roundToFiveSeconds)
    {
      int secondsModuloFive = randomSeconds % 5;
      if (secondsModuloFive != 0)
      {
        switch (this.timerDirection)
        {
          case DOWN:
            randomSeconds += 5 - secondsModuloFive;
            break;
          case UP:
            randomSeconds -= secondsModuloFive;
            break;
          default:
            throw new AssertionError();
        }
      }
    }

    int minutes = randomSeconds / 60;
    int secondsInMinute = randomSeconds % 60;
    return new TimeDTO(minutes, secondsInMinute);
  }

  /**
   * Check the answers for the last challenge for correctness
   * 
   * @param answer
   * @return <code>true</code> if the answer was right, else <code>false</code>
   */
  public boolean checkAnswerForLastChallenge(TimeDTO answer)
  {
    if (this.correctAnswer == null)
    {
      throw new IllegalStateException(
          "nextChallenge() needs to be called at least once before checkAnswerForLastChallenge(TimeDTO) is called.");
    }
    if (answer == null)
    {
      throw new IllegalArgumentException("answer may not be null.");
    }
    this.timeNeededForAnswer = this.stopWatch.stop();
    this.scoreForLastAnswer = this.scoringSchema.calculateScore(
        this.timerDirection, this.correctAnswer, answer,
        this.timeNeededForAnswer);
    this.score += this.scoreForLastAnswer;
    this.challengesDone++;
    return answer.equals(this.correctAnswer);
  }

  private TimeDTO calculateAnswer(ChallengeDTO challenge)
  {
    TimeDTO time = challenge.getTime();
    int secondsChallenge = time.getMinute() * 60 + time.getSecond();
    int secondsAnswer;
    switch (this.timerDirection)
    {
      case DOWN:
        secondsAnswer = secondsChallenge - challenge.getItem().getSeconds();
        break;
      case UP:
        secondsAnswer = secondsChallenge + challenge.getItem().getSeconds();
        break;
      default:
        throw new AssertionError();
    }
    int minuteAnswer = secondsAnswer / 60;
    int secondsInMinuteAnswer = secondsAnswer % 60;
    return new TimeDTO(minuteAnswer, secondsInMinuteAnswer);
  }

  public TimeDTO getAnswerForLastChallenge()
  {
    return this.correctAnswer;
  }

  /**
   * Returns the duration between the last call to nextChallenge() and the last
   * call checkAnswerForLastChallenge(TimeDTO). If nextChallenge() is called
   * after checkAnswerForLastChallenge(TimeDTO) has been called and
   * checkAnswerForLastChallenge(TimeDTO) has not yet been called again, these
   * calls to nextChallenge() have no effect on the Duration returned by this
   * method.
   * 
   * @return the duration between the last call to nextChallenge() and the last
   *         call checkAnswerForLastChallenge(TimeDTO)
   */
  public Duration getTimeNeededForLastAnswer()
  {
    if (this.timeNeededForAnswer == null)
    {
      throw new IllegalStateException(
          "nextChallenge() and checkAnswerForLastChallenge(TimeDTO) need to be called at least once before getTimeNeededForAnswer() is called.");
    }
    return this.timeNeededForAnswer;
  }

  public final float getScoreForLastAnswer()
  {
    return this.scoreForLastAnswer;
  }

  public final float getScore()
  {
    return this.score;
  }

  public final int getChallengesDone()
  {
    return this.challengesDone;
  }

  public void setTimerDirection(TimerDirection timerDirection)
  {
    this.timerDirection = timerDirection;
  }

  public TimerDirection getTimerDirection()
  {
    return this.timerDirection;
  }

  public void setItemDistribution(
      ProbabilisticDistribution<Item> itemDistribution)
  {
    this.itemDistribution = itemDistribution;
  }

  public void setRoundToFiveSeconds(boolean roundToFiveSeconds)
  {
    this.roundToFiveSeconds = roundToFiveSeconds;
  }
}
